/* nixonFTP
 * FTP client version 0.1
 * Copyright (C) 2010 NIXON Development Corporation.
 * All rights reserved.
 * http://members.shaw.ca/nixon.com
 */
package nixonftp;

import java.io.OutputStream;
import java.net.SocketAddress;
import java.util.Iterator;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import nixonftp.error.NXLoginException;
import nixonftp.error.NXProtocolException;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.InetSocketAddress;
import java.net.Proxy;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.Vector;
import sun.net.ftp.FtpLoginException;
import sun.net.ftp.FtpProtocolException;
import sun.net.ftp.impl.FtpClient;

public class NXFtpClient extends FtpClient implements NXSocketClient {

	private String[] arrCredentials;
	private HashMap<String, NXObjectIndex> objectIndex = new HashMap<String, NXObjectIndex>();
	private String dirListing = "";
	static int FTP_SUCCESS = 1;
	static int FTP_TRY_AGAIN = 2;
	static int FTP_ERROR = 3;
	String command;
	private boolean binaryMode = true;
	private boolean enableLog = false;
	private NXLog log;
	private String currDir = "/";
	private boolean stopCurrent = false;
	private String strWelcome;
	
	public NXFtpClient() {
		super();
	}

	public NXFtpClient(NXLog log, Proxy proxy) {
		super();
		setProxy(proxy);
	}

	public String getCurrDir() {
		if (currDir.equals("/")) return "";
		return currDir;
	}

	public boolean getBinary() {
		return binaryMode;
	}
	/**
	 * Sets the credentials for the session.
	 *
	 * @param argv An array of strings containing:
	 * 0: Server's hostname
	 * 1: Username
	 * 2: Password
	 */
	public void setCredentials(String[] argv) {
		arrCredentials = argv;
	}

	public String[] getCredentials() {
		return arrCredentials;
	}

	private int parsePort(String addr) {
		int pos = addr.lastIndexOf(":");
		int ret = -1;
		if (pos != -1 && !(pos == 3 && addr.startsWith("ftp://"))) {
			ret = Integer.parseInt(addr.substring(pos + 1));
		}
		return ret;
	}

	/**
	 * Connects to the server with the credentials set by calling SetCredentials.
	 * Be sure to have called SetCredentials before calling this.
	 */
	public void connect(String sgInitialDir) throws SocketException, FtpLoginException, FtpProtocolException, UnknownHostException, IOException {
		NXObjectIndex oi = new NXObjectIndex('d', sgInitialDir, "", "", "", -1, -1, new Date(0));
		objectIndex.put(sgInitialDir, oi);
		if (arrCredentials[0].equals("")) {
			arrCredentials[0] = "127.0.0.1";
		}
		int port = parsePort(arrCredentials[0]);
		String server = arrCredentials[0];
		if (port != -1) {
			server = arrCredentials[0].substring(0, arrCredentials[0].indexOf(":"));
			System.out.println("Using port " + port);
		} else {
			port = 21;
		}
		
		this.connect(new InetSocketAddress(server, port));
		if (arrCredentials[1].equals("")) {
			arrCredentials[1] = "anonymous";
			arrCredentials[2] = "ftp@nixon.com";
		}
		login(arrCredentials[1], arrCredentials[2].toCharArray());
		this.changeDirectory(sgInitialDir);
		indexDirectory(sgInitialDir);
	}

	private String getListingAsString() throws SocketException, IOException, NXProtocolException, NXLoginException, FtpProtocolException {
		InputStream ls = list(null);
		BufferedInputStream bls = new BufferedInputStream(ls);

		/*OutputStream os = new FileOutputStream("test.text");
		BufferedOutputStream bos = new BufferedOutputStream(os);*/
		ByteArrayOutputStream baols = new ByteArrayOutputStream();

		byte[] buffer = new byte[1024];
		int readCount;

		while ((readCount = ls.read(buffer)) > 0) {
			baols.write(buffer, 0, readCount);
		}

		ls.close();

		return baols.toString();
	}

	@SuppressWarnings("empty-statement")
	/**
	 * Adds the remote directory to the index.
	 */
	public int indexDirectory(String dir) throws SocketException, FtpProtocolException, IOException {
		try {
			changeFolder(dir);
		} catch (Exception ex) {
			ex.printStackTrace();
		}
		NXObjectIndex blankObject = new NXObjectIndex('d', "/", "", "", "", -1, -1, new Date(0));
		objectIndex.put(dir, blankObject);

		String[] listing = null;
		String all = null;
		
		all = getListingAsString();
		if (all.isEmpty()) return 0;
		listing = all.split("\n");

		char type;
		String name;
		int hardLinks;
		String permissions;
		String owner = null;
		String group = null;
		long size = 0;
		Date date;
		int arrow = 0;
		int idxSpace;
		String real = null;
		NXObjectIndex dirObj = (NXObjectIndex) objectIndex.get(dir);
		NXObjectIndex obj;

		for (int x = 0; x < listing.length; x++) {
			try {
				type = listing[x].charAt(0);
				permissions = listing[x].substring(1, 10);
				listing[x] = listing[x].substring(10).trim();

				idxSpace = listing[x].indexOf(" ");
				String strHardLinks = listing[x].substring(0, idxSpace);
				hardLinks = Integer.valueOf(strHardLinks);

				for (int y = 0; y < 3; y++) {
					listing[x] = listing[x].substring(idxSpace).trim();
					idxSpace = listing[x].indexOf(" ");
					String item = listing[x].substring(0, idxSpace);
					listing[x] = listing[x].substring(idxSpace + 1).trim();
					idxSpace = 0;

					switch (y) {
						case 0:
							owner = item;
							break;
						case 1:
							group = item;
							break;
						case 2:
							size = Long.valueOf(item);
							break;
					}
				}

				String strDate = listing[x].trim().substring(0, 12).trim();
				date = parseDate(strDate);

				name = listing[x].substring(12).trim();

				if (name.equals(".") || name.equals("..")) continue;

				if (type == 'l') {
					arrow = name.indexOf("->");
					if (arrow != -1) {
						real = name.substring(arrow + 3);
						name = name.substring(0, arrow - 1);
					}
				}
				obj = new NXObjectIndex(type, name, permissions, owner, group, size, hardLinks, date);

				if (real != null && !real.startsWith("/")) real = dir + "/" + real;
				obj.setRealLocation(real);
				dirObj.objects.put(name, obj);
			} catch (Exception ex) {
				System.out.println("Error in IndexDirectory: " + ex.toString());
			}
		}
		return all.length();
	}

	/**
	 * Make a fake directory listing from NXObjectIndex given for path.
	 */
	public void fakeDirectory(NXObjectIndex oi, String path) { //used for links
		objectIndex.put(path, oi);
	}

	/**
	 * Check if dir is a directory.
	 */
	public boolean isDirectory(String dir) {
		boolean isLinkDir = false;
		try {
			changeFolder(dir);
			isLinkDir = true;
		} catch (Exception ex) {
			if (ex instanceof FileNotFoundException) {
				isLinkDir = false;
			}
		}
		return isLinkDir;
	}

	/**
	 * Returns a new Date object from FTP listing date string.
	 */
	private Date parseDate(String date) {
		String temp;
		int month = 0;
		int day;
		int year;
		int hour;
		int minute;
		String[] months = {"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"};

		Calendar cal = Calendar.getInstance();
		year = cal.get(Calendar.YEAR);
		int currMonth = cal.get(Calendar.MONTH);
		cal.clear();

		temp = date.substring(0, 3); //get month
		for (int x = 0; x < months.length; x++) {
			if (months[x].equals(temp)) {
				month = x;
			}
		}

		temp = date.substring(4, 6);
		day = Integer.parseInt(temp.trim());

		if (date.contains(":")) { //includes time
			hour = Integer.parseInt(date.substring(8, 9));
			minute = Integer.parseInt(date.substring(11));
			int actualYear;
			if (month > currMonth) {
				actualYear = year - 1;
			} else {
				actualYear = year;
			}
			cal.set(actualYear, month, day, hour, minute);
		} else {
			year = Integer.parseInt(date.substring(8));
			cal.set(year, month, day);
		}
		return cal.getTime();
	}

	public HashMap getObjectIndex() {
		return objectIndex;
	}

	public void createDirectory(String dir) throws IOException {
		try {
			makeDirectory(dir);
		} catch (FtpProtocolException ex) {
			Logger.getLogger(NXFtpClient.class.getName()).log(Level.SEVERE, null, ex);
		}
	}
	
	public void deleteDirectory(String dir) throws IOException, FtpProtocolException {
		this.removeDirectory(dir);
	}

	public void delete(String file) throws IOException, FtpProtocolException {
		this.deleteFile(file);
	}

	/**
	 * Downloads a remote file.
	 *
	 * @param remoteFile The remote file.
	 * @param localFile The local file.
	 * @param preview If true, then downloads to byte array and then returns it.
	 * @param callBack Calls to update progress.
	 */
	public byte[] get(String remoteFile, String localFile, boolean preview, NXProgressUpdateRunnable callBack) throws IOException, FtpProtocolException {
		int i = 0;
		int downloaded = 0;
		FileOutputStream out = null;
		if (binaryMode) { this.setBinaryType(); } else { this.setAsciiType(); }
		if (!preview) {
			out = new FileOutputStream(localFile);
		}
		//long size = NXFormat.getSizeFromRemotePath(remoteFile, objectIndex);
		long size = ((NXObjectIndex)objectIndex.get(currDir).objects.get(remoteFile)).size;
		BufferedInputStream in = null;
		byte[] bytesIn = new byte[4096];
		byte[] bytesOut = null;
		if (preview) {
			bytesOut = new byte[(int) size];
		}
		double progress;

		in = new BufferedInputStream(this.getFileStream(remoteFile));
		
		callBack.size = size;
		if (preview) {
			while (downloaded < size
					&& (i = in.read(bytesOut, downloaded, bytesOut.length - downloaded)) >= 0 && !stopCurrent) {
				downloaded += i;
				callBack.bytes = downloaded;
				callBack.add = i;
				callBack.run();
			}
		} else {
			while ((i = in.read(bytesIn)) >= 0 && !stopCurrent) {
				downloaded += i;
				out.write(bytesIn, 0, i);
				callBack.bytes = downloaded;
				callBack.add = i;
				callBack.run();
			}
		}
		completePending();
		callBack.run();
		in.close();
		if (out != null) out.close();
		if (stopCurrent) {
			//stop!
			stopCurrent = false;
		}
		return bytesOut;
	}

	public void put(String localFile, String remoteFile, NXProgressUpdateRunnable callBack) throws IOException, FtpProtocolException {
		int i = 0;
		long uploaded = 0;
		byte[] bytesIn = new byte[1024];
		if (binaryMode) { this.setBinaryType(); } else { this.setAsciiType(); }
		File file = new File(localFile);
		long size = file.length();
		
		FileInputStream in = new FileInputStream(localFile);
		BufferedOutputStream out = null;

		out = new BufferedOutputStream(this.putFileStream(remoteFile));
		while ((i = in.read(bytesIn)) >= 0) {
			uploaded += i;
			out.write(bytesIn, 0, i);
			callBack.bytes = uploaded;
			callBack.add = i;
			callBack.run();
		}
		in.close();
		out.close();
	}

	public void disconnect() {
		try {
			this.close();
		} catch (IOException ex) {
			ex.printStackTrace();
		}
	}

	@Override
	public void changeFolder(String dir) throws Exception {
		if (!currDir.equals(dir)) {
			if (dir.isEmpty()) dir = "/";
			this.changeDirectory(dir);
		}
		currDir = dir;
	}

	public void stopCurrent() {
		stopCurrent = true;
	}

	public String getWelcome() {
		return this.getWelcomeMsg() + '\n' + strWelcome;
	}

	public void cdUp() throws IOException {
		throw new UnsupportedOperationException("Not supported yet.");
	}

	public void renameImpl(String oldFile, String newFile) throws IOException, FtpProtocolException {
		this.rename(oldFile, newFile);
	}
}

